using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using LibGreen.Jabber.Utility;

// Ambiguous cref in the private constructor
#pragma warning disable 419

namespace LibGreen.Jabber.ProtocolElements
{
    /// <summary>
    /// Describes the basic parts of an XMPP element
    /// </summary>
    public sealed class Element
    {
        #region Element ID generation and caching
        private static int nextId = 0;
        private static object locker = new object();
        private static Dictionary<string, Element> elementCache =
            new Dictionary<string, Element>();

        /// <summary>
        /// Returns the next element ID in the sequence
        /// </summary>
        /// <returns></returns>
        private static int NextID()
        {
            lock (locker)
            {
                return ++nextId;
            }
        }

        /// <summary>
        /// Caches a newly created <see cref="Element"/> by its ID attribute
        /// </summary>
        /// <param name="element">The <see cref="Element"/> to cache</param>
        /// <returns>A reference to the cached <see cref="Element"/></returns>
        private static Element CacheElement(Element element)
        {
            lock (elementCache)
            {
                elementCache.Add(element.Attributes["id"], element);
            }
            return element;
        }

        /// <summary>
        /// Removes and returns a cached <see cref="Element"/>
        /// </summary>
        /// <param name="id">The ID attribute of the <see cref="Element"/> to look up</param>
        /// <returns>The decached <see cref="Element"/>, or <c>null</c> if no element by that ID was found in the cache</returns>
        private static Element DecacheElement(string id)
        {
            Element retval = null;
            lock (elementCache)
            {
                if (elementCache.ContainsKey(id))
                {
                    retval = elementCache[id];
                    elementCache.Remove(id);
                }
            }
            return retval;
        }

        #endregion

        /// <summary>
        /// Creates and caches a new Element with the specified name and set of attributes
        /// </summary>
        /// <param name="name">The name of the element</param>
        /// <param name="attributes">The XML attributes of the element</param>
        /// <returns>A new <see cref="Element"/></returns>
        /// <remarks>This method adds (or replaces) an "id" attribute to the <paramref name="attributes"/> parameter</remarks>
        public static Element CreateElement(string name, Dictionary<string, string> attributes)
        {
            return CreateElement(name, attributes, "");
        }

        /// <summary>
        /// Initializes a new Element with the specified name, set of attributes, and value
        /// </summary>
        /// <param name="name">The name of the element</param>
        /// <param name="attributes">The XML attributes of the element</param>
        /// <param name="value">The value of the element</param>
        /// <returns>A new <see cref="Element"/></returns>
        public static Element CreateElement(string name, Dictionary<string, string> attributes, string value)
        {
            attributes["id"] = NextID().ToString();
            return CacheElement(new Element(name, attributes, value));
        }

        private readonly string name;
        private readonly Dictionary<string, string> attributes;
        private readonly List<Element> children = new List<Element>();
        private readonly Element originatingElement;
        private readonly string value;
        private bool isChildElement;

        #region Constructors
        /// <summary>
        /// Initializes a new Element with a specified name
        /// </summary>
        /// <param name="name">The name of the element</param>
        /// <param name="attributes">The XML attributes of the element</param>
        /// <param name="value">The value of the element</param>
        /// <remarks>This constructor cannot be used directly.  To create a new Element,
        /// use the <see cref="Element.CreateElement"/> methods.</remarks>
        private Element(string name, Dictionary<string, string> attributes, string value)
        {
            this.name = name;
            this.value = value;
            this.attributes = attributes;
        }

        /// <summary>
        /// Initializes a new Element from a received <see cref="XmlReader"/> stream
        /// </summary>
        /// <param name="xmlReader">An <see cref="XmlReader"/> positioned at the start of the element</param>
        /// <remarks><paramref name="xmlReader"/> will be positioned over the corresponding end element after the constructor returns</remarks>
        public Element(XmlReader xmlReader)
        {
            // Some stray whitespace has been observed coming from Google Talk, consume it
            if (xmlReader.NodeType == XmlNodeType.Whitespace)
            {
                xmlReader.Read();
            }

            this.name = xmlReader.Name;

            // Read the attributes out of the current element
            attributes = new Dictionary<string, string>();
            if (xmlReader.HasAttributes)
            {
                while (xmlReader.MoveToNextAttribute())
                {
                    attributes.Add(xmlReader.Name, xmlReader.Value);
                }
            }

            // Decache the originating element for future reference
            if (attributes.ContainsKey("id"))
            {
                originatingElement = DecacheElement(attributes["id"]);
            }

            // If the current element has no value and no children, skip the rest of the processing.
            if (!xmlReader.IsEmptyElement)
            {
                // Consume the value of the element
                this.value = xmlReader.ReadString();

                // Special case in XMPP:  The <stream:stream> element doesn't close, and has
                // children that we don't want to consume here. Skip processing and continue.
                if (this.name != "stream:stream")
                {
                    // This is a little confusing and was hands-down the hardest part of this
                    // parser to get correct.  Thank god for unit tests.
                    //
                    // The first check verifies that if we are on an empty element after consuming the string value,
                    // and it is the *same* element that was just being read, skip past it and end processsing.
                    // The reason for this is because the XmlReader doesn't see <node /> as an EndElement, so
                    // relying on the second check only doesn't work.  The second check is to see whether we're now
                    // on an end element after consuming the string value. If we are, there were no children
                    // to the node and processing can end.
                    if ((xmlReader.IsEmptyElement && xmlReader.Name == name) || xmlReader.NodeType == XmlNodeType.EndElement)
                    {

                    }
                    else
                    {
                        // Loop until we read the end element of the node that's currently processing.
                        while (!(xmlReader.NodeType == XmlNodeType.EndElement && xmlReader.Name == name))
                        {
                            if (xmlReader.NodeType == XmlNodeType.Element)
                            {
                                // Consume the subtree starting with the current node, position
                                // the subtree reader on the newly started element, and create the child.
                                using (XmlReader subreader = xmlReader.ReadSubtree())
                                {
                                    subreader.Read();
                                    children.Add(new Element(subreader));
                                }
                            }
                            // Consume (a) the end element of the subtree, which the reader will be positioned
                            // on after the subtree processing, or (b) some random element if the subtree
                            // processing didn't happen.
                            xmlReader.Read();
                        }
                    }
                }
            }

            //Logging.LogString("Received element " + name);
        }
        #endregion

        #region Properties
        /// <summary>
        /// Gets the name of the element
        /// </summary>
        public string Name
        {
            get { return name; }
        }

        /// <summary>
        /// Gets the XML attributes of the element
        /// </summary>
        public Dictionary<string, string> Attributes
        {
            get { return attributes; }
        }

        /// <summary>
        /// Gets the value of the element
        /// </summary>
        public string Value
        {
            get { return value; }
        }

        /// <summary>
        /// Gets the child <see cref="Element"/>s of this element
        /// </summary>
        public List<Element> Children
        {
            get { return children; }
        }

        /// <summary>
        /// Gets the <see cref="Element"/> that caused the creation of this element,
        /// or <c>null</c> if the current element originated from the client or unprompted from the server
        /// </summary>
        public Element OriginatingElement
        {
            get { return originatingElement; }
        }

        /// <summary>
        /// Gets or sets a value indicating whether this is a child element
        /// </summary>
        /// <remarks>Child elements cannot have IDs. Setting IsChildElement to <c>true</c> strips the element's
        /// ID attribute and removes it from the active element cache</remarks>
        private bool IsChildElement
        {
            get { return isChildElement; }
            set
            {
                isChildElement = value;
                if (isChildElement == true)
                {
                    if (attributes.ContainsKey("id"))
                    {
                        DecacheElement(attributes["id"]);
                        attributes.Remove("id");
                    }
                }
            }
        }

        #endregion

        /// <summary>
        /// Gets a string representation of this element and its children suitable for network transmission
        /// </summary>
        /// <returns>A string to be sent to the XMPP server</returns>
        public override string ToString()
        {
            StringBuilder builder = new StringBuilder();
            builder.AppendFormat("<{0} ", name);
            foreach (KeyValuePair<string, string> kvp in attributes)
            {
                builder.AppendFormat("{0}='{1}' ", kvp.Key, kvp.Value);
            }

            if (!String.IsNullOrEmpty(value))
            {
                builder.AppendFormat(">{0}</{1}", value, name);
            }
            else if (children.Count > 0)
            {
                builder.Append(">");
                foreach (Element element in children)
                {
                    element.IsChildElement = true;
                    builder.Append(element.ToString());
                }
                builder.AppendFormat("</{0}", name);
            }
            else
            {
                // Don't close client-sent <stream:stream> tags
                if (name != "stream:stream")
                {
                    builder.Append("/");
                }
            }

            return builder.Append(">").ToString();
        }
    }
}
